spb/market-atlas
Public
TypeScript 96.7%
SQL 1.6%
CSS 0.8%
JavaScript 0.5%
1import type { Metadata } from "next";2import Link from "next/link";3import { notFound } from "next/navigation";4import { PriceChart } from "@/components/market/chart";5import { EventRow } from "@/components/market/event-row";6import { InstrumentHeader } from "@/components/market/instrument-header";7import { InstrumentStats } from "@/components/market/instrument-stats";8import { CoverageBadge } from "@/components/market/coverage-badge";9import { ProvenancePanel } from "@/components/market/provenance-panel";10import { RightsBadge, StatusBadge } from "@/components/ui/status-badge";11import { Empty, Kv, Section } from "@/components/ui/section";12import { api, apiOptional } from "@/lib/api";13import { ASSET_CLASS_LABEL, formatDateTime, instrumentHref } from "@/lib/format";14import type { Exchange, Filing, Instrument, InstrumentCoverage, MarketEvent, Quote } from "@/lib/types";1516export const dynamic = "force-dynamic";1718interface Detail {19 instrument: Instrument;20 company: { id: string; name: string; cik: string | null; country: string | null; sector: string | null; industry: string | null; website: string | null } | null;21 exchange: (Exchange & { status: NonNullable<Exchange["status"]> }) | null;22 quote: Quote | null;23 aliases: Array<{ alias: string; source_id: string | null }>;24 sources: string[];25 related: Instrument[];26}2728async function load(id: string) {29 return api<Detail>(`/v1/instruments/${encodeURIComponent(id)}`);30}3132export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise<Metadata> {33 const { id } = await params;34 try {35 const d = await load(id);36 const i = d.instrument;37 const title = `${i.symbol} · ${i.name}`;38 const desc = `${i.name} (${i.symbol}) — ${ASSET_CLASS_LABEL[i.asset_class] ?? i.asset_class}${d.exchange ? ` on ${d.exchange.name}` : ""}. Canonical price, sources, confidence, history and events on Market Atlas.`;39 return { title, description: desc, alternates: { canonical: instrumentHref(i.id) }, openGraph: { title, description: desc, type: "website" } };40 } catch {41 return { title: "Instrument" };42 }43}4445export default async function InstrumentPage({ params }: { params: Promise<{ id: string }> }) {46 const { id } = await params;47 const d = await load(id);48 if (!d?.instrument) notFound();49 const i = d.instrument;50 const [events, filings, coverage] = await Promise.all([51 apiOptional<MarketEvent[]>(`/v1/events?instrument=${encodeURIComponent(i.id)}&limit=25`),52 d.company?.cik ? apiOptional<Filing[]>(`/v1/filings?cik=${encodeURIComponent(d.company.cik)}&limit=15`) : Promise.resolve(null),53 apiOptional<InstrumentCoverage>(`/v1/coverage/${encodeURIComponent(i.id)}`),54 ]);55 const isRate = i.asset_class === "TREASURY" || i.asset_class === "INTEREST_RATE" || i.asset_class === "BOND";56 const jsonLd = {57 "@context": "https://schema.org",58 "@type": "FinancialProduct",59 name: i.name,60 tickerSymbol: i.symbol,61 category: ASSET_CLASS_LABEL[i.asset_class] ?? i.asset_class,62 url: `${process.env.NEXT_PUBLIC_SITE_URL ?? "https://www.market-atlas.co"}${instrumentHref(i.id)}`,63 ...(d.exchange ? { provider: { "@type": "Organization", name: d.exchange.name } } : {}),64 };65 return (66 <div>67 <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />68 <InstrumentHeader instrument={i} quote={d.quote} exchange={d.exchange} coverage={coverage ?? undefined} />69 <div className="mx-auto max-w-[1440px] px-3 py-5 sm:px-5">70 <div className="grid grid-cols-1 [&>*]:min-w-0 gap-6 lg:grid-cols-[1fr_360px]">71 <div className="min-w-0">72 <PriceChart instrumentId={i.id} assetClass={i.asset_class} defaultRange={i.asset_class === "CRYPTO" ? "1D" : "3M"} />73 <Section title="Key statistics" hint={isRate ? "percent" : d.quote?.currency ?? undefined}>74 <InstrumentStats instrument={i} quote={d.quote} />75 </Section>76 <Section title="Why this price?" hint="source coverage matrix · every observation, its type, its role and its distance to the consensus" badge={coverage ? <CoverageBadge coverage={coverage} compact className="ml-1" /> : undefined}>77 <ProvenancePanel instrumentId={i.id} assetClass={i.asset_class} defaultOpen />78 </Section>79 <Section title="Recent events" href={`/events?instrument=${encodeURIComponent(i.id)}`} hint="canonical events touching this instrument">80 {events?.length ? (81 <ul className="rounded-md border border-rule bg-surface px-3">82 {events.map((e) => (83 <EventRow key={e.id} e={e} />84 ))}85 </ul>86 ) : (87 <Empty>No canonical events for {i.symbol} yet. Events are derived from observed moves, halts, filings and source incidents.</Empty>88 )}89 </Section>90 {d.company?.cik && (91 <Section title="Regulatory filings" href={`/filings?cik=${d.company.cik}`} hint="SEC EDGAR · linked by CIK">92 {filings?.length ? (93 <div className="overflow-x-auto rounded-md border border-rule bg-surface">94 <table className="table-dense">95 <thead>96 <tr>97 <th>Filed</th>98 <th>Form</th>99 <th>Filer</th>100 <th>Document</th>101 </tr>102 </thead>103 <tbody>104 {filings.map((f) => (105 <tr key={f.id}>106 <td className="mono text-ink-2">{formatDateTime(f.filed_at, { tz: "America/New_York" })}</td>107 <td className="mono font-medium">{f.form_type}</td>108 <td>{f.company_name}</td>109 <td>110 <a href={f.url} target="_blank" rel="noopener noreferrer" className="text-accent hover:underline">111 EDGAR ↗112 </a>113 </td>114 </tr>115 ))}116 </tbody>117 </table>118 </div>119 ) : (120 <Empty>No filings observed for CIK {d.company.cik} since Market Atlas started listening.</Empty>121 )}122 </Section>123 )}124 </div>125 <aside className="space-y-6">126 <div className="rounded-md border border-rule bg-surface p-4">127 <h3 className="text-[11px] font-medium uppercase tracking-wide text-ink-3">Instrument</h3>128 <Kv129 cols={1}130 className="mt-2"131 items={[132 ["Market Atlas id", i.id],133 ["Class", ASSET_CLASS_LABEL[i.asset_class] ?? i.asset_class],134 ["Security type", i.security_type?.replace(/_/g, " ").toLowerCase() ?? "—"],135 ["Currency", i.currency ?? "—"],136 ["Country", i.country ?? "—"],137 ...(i.base ? ([["Base / quote", `${i.base} / ${i.quote ?? "—"}`]] as Array<[string, string]>) : []),138 ["MIC", i.mic ?? "—"],139 ]}140 />141 {d.quote && (142 <div className="mt-3 flex flex-wrap gap-1.5">143 <StatusBadge status={d.quote.data_status} />144 <RightsBadge status={d.quote.rights_status} />145 </div>146 )}147 </div>148 {d.exchange && (149 <div className="rounded-md border border-rule bg-surface p-4">150 <h3 className="text-[11px] font-medium uppercase tracking-wide text-ink-3">Venue</h3>151 <Link href={`/exchanges/${d.exchange.id}`} className="mt-1 block font-medium hover:underline">152 {d.exchange.name}153 </Link>154 <div className="mt-1 flex items-center gap-2 text-sm text-ink-2">155 <StatusBadge status={d.exchange.status.state} /> local {d.exchange.status.localTime} · {d.exchange.timezone}156 {d.exchange.status.isHoliday && d.exchange.status.holidayName ? ` · ${d.exchange.status.holidayName}` : ""}157 </div>158 {d.exchange.status.nextTransition && (159 <div className="mt-1 text-xs text-ink-3">160 Next: {d.exchange.status.nextTransition.state.toLowerCase()} at {formatDateTime(d.exchange.status.nextTransition.at, { tz: d.exchange.timezone })}161 </div>162 )}163 </div>164 )}165 {d.company && (166 <div className="rounded-md border border-rule bg-surface p-4">167 <h3 className="text-[11px] font-medium uppercase tracking-wide text-ink-3">Company</h3>168 <div className="mt-1 font-medium">{d.company.name}</div>169 <Kv cols={1} className="mt-1" items={[["CIK", d.company.cik ?? "—"], ["Country", d.company.country ?? "—"], ["Sector", d.company.sector ?? "—"]]} />170 {d.company.cik && (171 <a href={`https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany&CIK=${d.company.cik}`} target="_blank" rel="noopener noreferrer" className="mt-2 inline-block text-xs text-accent hover:underline">172 EDGAR company page ↗173 </a>174 )}175 </div>176 )}177 <div className="rounded-md border border-rule bg-surface p-4">178 <h3 className="text-[11px] font-medium uppercase tracking-wide text-ink-3">Observed by</h3>179 {coverage && <CoverageBadge coverage={coverage} className="mt-1.5" />}180 {d.sources.length ? (181 <ul className="mt-1 flex flex-wrap gap-1.5">182 {d.sources.map((s) => (183 <li key={s}>184 <Link href={`/sources#${s}`} className="mono rounded border border-rule px-1.5 py-0.5 text-xs text-ink-2 hover:border-rule-strong">185 {s}186 </Link>187 </li>188 ))}189 </ul>190 ) : (191 <p className="mt-1 text-xs text-ink-3">No source has published a value for this instrument in the current process yet.</p>192 )}193 {d.aliases.length > 0 && (194 <>195 <h3 className="mt-3 text-[11px] font-medium uppercase tracking-wide text-ink-3">Aliases</h3>196 <ul className="mt-1 flex flex-wrap gap-1.5">197 {d.aliases.slice(0, 12).map((a) => (198 <li key={`${a.alias}-${a.source_id}`} className="mono rounded bg-surface-2 px-1.5 py-0.5 text-xs text-ink-2" title={a.source_id ? `alias used by ${a.source_id}` : "global alias"}>199 {a.alias}200 {a.source_id ? <span className="text-ink-3"> · {a.source_id}</span> : null}201 </li>202 ))}203 </ul>204 </>205 )}206 </div>207 {d.related.length > 0 && (208 <div className="rounded-md border border-rule bg-surface p-4">209 <h3 className="text-[11px] font-medium uppercase tracking-wide text-ink-3">Related instruments</h3>210 <ul className="mt-1 divide-y divide-rule text-sm">211 {d.related.map((r) => (212 <li key={r.id} className="flex items-center justify-between py-1.5">213 <Link href={instrumentHref(r.id)} className="min-w-0 truncate hover:underline">214 <span className="mono font-medium">{r.symbol}</span> <span className="text-xs text-ink-3">{r.name}</span>215 </Link>216 <span className="text-[10.5px] uppercase text-ink-3">{r.exchange_id ?? r.asset_class}</span>217 </li>218 ))}219 </ul>220 </div>221 )}222 </aside>223 </div>224 </div>225 </div>226 );227}228